JS Advanced --變數


Posted by s103071049 on 2021-07-26

先從變數開始談起

JS 共有七種資料型態,可以七種型態可以分成兩類,primitive type 和 object。

七種資料型態

primitive type
1. null
2. undefined
3. string
4. number
5. boolean
6. symbol (ES6)

其他都是 object
7. object(array, function, date ...)

一、檢視資料型態

方法一、typeof (用 typeof 並不能保證結果為這七種型態),會傳回一個字串值。

console.log(typeof function() {}) // function
console.log(typeof null) // object 廣為人知的 js bug
  • 說明:JavaScript 的值就總以型別標簽跟著一個值的方式表示。物件的型別標簽是 0. 而 null 這個值是使用 NULL 指標 (在大部份平台上是 0x00) 來表示. 因此, null 看起來像是一個以 0 為型別標簽的值, 並使得 typeof 傳回不甚正確的結果
// 宣告變數但甚麼都沒給他
var a 
console.log(typeof a) // undefined

// 沒有宣告變數
console.log(typeof b) // undefined
console.log(b) // ReferenceError: b is not defined

// 檢測變數是否有用
var c = 100
if (typeof c !== 'undefined') {
  console.log(c)
}
  • 用途:檢測變數是否有用、檢測 function

方法二、Array.isArray (回傳 true/false 告訴你是否為 array)

console.log(typeof []) // object
console.log(Array.isArray([])) // true

方法三、Object.prototype.toString.call() 會將型態判斷更細

console.log(Object.prototype.toString.call('1')) // [object String]
console.log(Object.prototype.toString.call([1, 2, 3])) // [object Array]
console.log(Object.prototype.toString.call(new Date())) // [object Date]
console.log(Object.prototype.toString.call(null)) // [object Null]
  • 說明:有些 library 會用這個檢視型態,對於較舊的瀏覽器並沒有 Array.isArray,所以會利用 Object.prototype.toString.call() 去檢視結果是否同 [object Array]

二、primitive type VS object type

primitive types 是存值,object types 是存記憶體位置,再依據記憶體位置衍伸去改值。

差別一、immutable or not

primitive type is immutable(不能改變)。primitive types 會回傳一個新的值而非直接改變變數,object type 可以改變於原本的值。

因此,對 object type 做操作時,很多都會改變原本的值,要看文件知道這個 method 做了甚麼事情。

var a = 1
a = 2 // 重新賦值
console.log(a) // 2

var str = 'aaaaa'
str.toUpperCase() // 回傳一個新的字串,而非改變自己
console.log(str) // aaaaa

let newStr = str.toUpperCase() // primitive type 會回傳一個新的值
console.log(newStr) // AAAAA

var arr = [1]
arr.push(2) // 真的改變 arr 從 [1] => [1, 2]
console.log(arr) // [ 1, 2 ]

差別二、賦值 (=)

var a = 10
var b = a
console.log(a, b) // 10 10
b = 20
console.log(a, b) // 10 20

======
var obj = {
  number : 10
}

var obj2 = obj
console.log(obj, obj2) // { number: 10 } { number: 10 }
obj2.number = 20
console.log(obj, obj2) // { number: 20 } { number: 20 }

因為底層的運作,導致兩類在賦值上有截然不同的反應。對 primitive types 他是直接存值在記憶體。objective types 存的值是記憶體位置,當我對 object types 用 = 賦值,他會先給一塊記憶體,將記憶體位置的值放進來。用點的話,表示我要存取他指到記憶體底下的某個屬性,

var a = 10
// 記憶體 a : 10
var b = a
// 將 a 的值 copy 過去給 b,所以 b: 10
b = 20
// b : 20
var obj = {
  number : 10
}
/*
  obj : 0x01 
  0x01: {
    number: 10
  }
*/

var obj2 = obj
// obj2: 0x01

obj2.number = 20

/*
  obj : 0x01
  obj2 : 0x01 
  0x01: {
    number: 20
  }
*/

因為 array 也是 object type,所以

var arr = []
var arr2 = arr

console.log(arr, arr2) // [] []
arr2.push(123)
console.log(arr, arr2) // [ 123 ] [ 123 ]
  1. 當我宣告 arr = [ ],底層 arr 存放的是記憶體位置 arr:0x01,其中 0x01 這個位置放的是 [ ]
  2. 宣告 arr2 = arr,將 arr 的記憶體位置複製過來
  3. arr2.push(123) 是對 0x01 這個記憶體位置 push,現在 0x01 這個記憶體位置放的是 [123]
  4. 因為 arr, arr2 指向相同的記憶體位置,所以 log 出來的值相同。
var arr = []
var arr2 = arr

console.log(arr, arr2) // [] []
arr2 = [123]
console.log(arr, arr2) // [] [ 123 ]
  1. arr = [ ] => arr:0x01, 其中 0x01:[ ]
  2. arr2 = arr => arr2: 0x01,其中 0x01:[ ]
  3. arr2 = [123] => arr2: 0x02,其中 0x02:[123],也就是他賦值是指向一塊新的記憶體
  4. 因為 arr, arr2 指向不同記憶體位置,所以兩個 log 出的值會不同

使用等號常見疏漏

  1. var a = 10
  2. 執行到第三行,將 a 賦值為 20,看 if 條件然後 log 出 hi
var a = 10
if (a = 20) {
  console.log('hi')
}
// 結果會是 hi,等同下方代碼
a = 20
if (a) {
  console.log('hi')
}

三、== 與 === 的差別

兩個等號會轉換型態,因為三個等號不會轉換型態,所以型態不同答案就是 false。
推薦:每次都用 ===

console.log(2 === '2') // false
console.log(2 == '2') // true

差別三、===

對 object types 而言, === 成立只有在比較的變數都指向相同記憶體位置才成立。

var obj = {
  number: 1
}

var obj2 = obj
obj2.number = 2

console.log(obj === obj2) // true
  1. obj:0x01 其中 0x01: {number:1}
  2. obj2 = obj 所以 obj2:0x01 其中 0x01: {number:1}
  3. obj2.number = 2 所以指到的記憶體位置 0x01: {number:2}
  4. object types 做 === 比較的會是記憶體位置,而非實際的值
  5. 記憶體位置都是 0x01 所以是 true

同理,array is also object types.

var arr = [1]
var arr2 = [1]
console.log(arr === arr2) // false

var arr3 = []
var arr4 = []
console.log(arr3 === arr4) //false
  1. var arr = [1],arr:0x01 其中 0x01: [1] (將記憶體位置給 arr 讓他可以指到陣列去)
  2. var arr2 = [1],arr:0x02 其中 0x02: [1]
  3. 因為 === 比較的是記憶體位置,所以儘管記憶體位置裡的值相同,但仍印出 false
arr = [1]
arr2 = [1]
arr2 = arr
console.log(arr === arr2) // true

object types 做等號比較就是看他有沒有指向同樣地記憶體位置

console.log([] === []) // false
console.log({} === {}) // false

例外:NAN

NAN (not a number 其實還是一個 number),NAN 不等於任何東西,even 他自身

console.log(typeof NAN) // number
var a = Number('xxx')
console.log(a) // NAN

console.log( a === a) // false

可以透過 isNAN() 去知道他是不是 NAN,但這個函式在舊的瀏覽器並不支援。

參考資料:
js eqiality table

初次見面:let 與 const

一旦以 const 進行宣告,他的值就不能改變。以 const 宣告時就要一併給他初始值,不能之後再賦值。let、var 沒有上述雜七雜八的限制。

對 object types 而言,const 是鎖定記憶體位置,並不是鎖定記憶體位置裡的東西。

const b = {
  number : 1
}

b.number = 100 // 沒有錯誤提示
b = {number : 3} // TypeError: Assignment to constant variable.
  1. b:0x01 其中 0x01 : {number : 1}
  2. b.number = 100 是去記憶體位置 0x01 將 number 改成 100。也就是 b: 0x01 其中 0x01: {number: 100}
  3. b = {number : 3},重給一塊記憶體位置並在裡面放。也就是 b:0x02 其中 0x02: {number : 3}
const arr = [1, 2, 54]
arr.push('1')
arr[0] = 3 // 沒有改變 arr 記憶體位置,而是改變他指的那個記憶體位置的那個值
arr = [3] // TypeError: Assignment to constant variable
  1. const arr = [1, 2, 54] 也就是 arr: 0x01 其中 0x01: [1, 2, 54]
  2. arr[0] = 3 也就是 arr: 0x01 其中 0x01: [3, 2, 54]
  3. arr = [3] 也就是 arr: 0x02 其中 0x02: [3] 因為 const 的規則不允許,所以會出現 TypeError: Assignment to constant variable

三、變數的生存範圍:Scope

作用域也就是變數的生存範圍,一出這個範圍就無法存取到這個變數。scope chain 會依照程式碼的放置的位置而定,跟我在哪裡呼叫 function 無關。

我們會先聚焦在 ES6 以前的作用域。

ES6 以前變數的作用域基本單位是 function,只有 function 可以產生新的作用域。

a 宣告在 test scope 裡面,一旦出了 function,就不能被其他人看到,所以對於外面來說因為找不到 a,a 是 undefined

function test() {
  var a = 10 // a 宣告在 test scope 裡面
  console.log(a)
}

test()
console.log(a) // ReferenceError: a is not defined

全域變數可以想成是以檔案為單位,因為作用域是往上找,當 test 引擎在 test scope 裡面找不到 a,他就往上找。

var a = 20 // global variable

function test() {
  console.log(a) // 20
}

test()
console.log(a) // 20

因為會在 function 的作用域先找,所以 test 裡面的 a 是 10

var a = 20 // global variable

function test() {
  var a = 10
  console.log(a) // 10
}

test()
console.log(a) // 20

test 裡面的 a = 10,但沒有變數宣告,也就是沒有在 test scope 新增一個變數。往上層找,發現 global 有宣告 a,就將 global a 的值改成 10

var a = 20 // global variable

function test() {
   a = 10
  console.log(a) // 10
}

test()
console.log(a) // 10

同樣在 test() 裡面 a = 10,往上層找也找不到值,就自動幫我在 global 宣告變數。所以這時 a 被宣告成 global variable。
在 function 內使用變數一定要宣告,不然會變成 global variable。

function test() {
   a = 10 // global variable
  console.log(a) // 10
}

test()
console.log(a) // 10

往上找的過程構成了 scope chain。
inner scope => test scope => global scope

var a = 'global' // global scope

function test() { // scope
  var a = 'test scope a'
  var b = 'test scope b'
  console.log(a, b) // 'test scope a' 'test scope b'
  function inner() { // scope
    var b = 'inner scope b'
    console.log(a, b) // 'test scope a' 'inner scope b'
  }
  inner()
}

test()
console.log(a) // 'global'

作用域的判斷和在哪邊宣告有關係,和在哪邊呼叫 function 沒有關係。這個 test 和 change 是兩個平行的地方。
test scope -> global scope

var a = 'global' // global scope

function change() { // change scope : a
  var a = 10
  test()
}

function test() { // test scope
  console.log(a)
}

change() // 'global'

同理,test scope -> global scope

var a = 'global' // global scope

function change() { // change scope
  var a = 10
  function inner() { // inner scope
    var a = 'inner'
    test()
  }
  inner()
}

function test() { // test scope
  console.log(a)
}

change() // 'global'

四、let 與 const 的生存範圍

ES6 引進 let, const。因此 ES6 以前的作用域跟 ES6 以後略有不同。

  • ES6 前 : 作用域以 function 為基礎
  • ES6 以後 : let, const 宣告的變數,只要有 block { } 就會產生作用域

用 var 宣告,他的 scope 就是 test 這個 function。所以在 test 裡都可以用到 b 這個變數。

function test() {
  var a = 60
  if (a === 60) {
    var b = 10
  }
  console.log(b)
}

test() // 10

let 或 const 宣告變數,scope 會存在於 block 內。以這個例子 b 只有在 if 裡面才活著,出了 if 就存取不到。

function test() {
  var a = 60
  if (a === 60) {
    let b = 10
  }
  console.log(b)
}

test() // ReferenceError: b is not defined

因為 i 是用 var 宣告,所以在 test 裡面都有作用
i = 9 進入下一圈迴圈 i++ => i = 10,判斷條件 10 < 10 false,

function test() {

  for(var i=0; i<10; i++) {
    console.log('i', i)
  }
  console.log('final value', i) // 10
}

test()

i 用 let 宣告,所以 i 只存在 for loop 的 block 中。一脫離就存取不到這個 i。

function test() {

  for(let i=0; i<10; i++) {
    console.log('i', i)
  }
  console.log('final value', i) // ReferenceError: i is not defined
}

test()

重點整理

  1. var : function scope
  2. let/const : block scope

#scope #var #let #const #variables in js







Related Posts

How to solve the perpetual loading issue in Evernote? Evernote 一直轉圈圈的解決辦法

How to solve the perpetual loading issue in Evernote? Evernote 一直轉圈圈的解決辦法

Day 103

Day 103

17. Iterator

17. Iterator


Comments